package gps.api;
import gps.Heuristic;
import gps.Parameters;
import gps.SearchStrategy;
import gps.exception.NotAppliableException;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public abstract class GPSEngine {

	protected List<GPSNode> open = new ArrayList<GPSNode>();

	protected List<GPSNode> closed = new ArrayList<GPSNode>();
	
	protected Map<GPSState,GPSNode> usedHash = new HashMap<GPSState,GPSNode>();

	protected GPSProblem problem;

	// Use this variable in the addNode implementation
	protected SearchStrategy strategy;

	public void engine(GPSProblem myProblem, SearchStrategy myStrategy, Heuristic myHeuristic) {

		System.out.println("Chosen strategy: " + myStrategy);
		System.out.println("Chosen heuristic: " + myHeuristic);
		System.out.println();

		problem = myProblem;
		strategy = myStrategy;

		GPSState initState = problem.getInitState();
		GPSNode rootNode = new GPSNode(initState, 0, 0);

		boolean finished = false;
		boolean failed = false;
		long explosionCounter = 0;
		long totalTime = System.currentTimeMillis();
		int iterDepthLimit = 2;		// Iterative Depth search initial limit.
		int depthLimit;
		
		// computes depth limit for the search
		if (strategy.equals(SearchStrategy.IDS)) {
			depthLimit = Math.min(iterDepthLimit, Parameters.MAX_DEPTH);
		} else {
			depthLimit = Parameters.MAX_DEPTH;
		}

		open.add(rootNode);
		//openHash.put(rootNode.getState(),rootNode);
		usedHash.put(rootNode.getState(),rootNode);
		while (!failed && !finished) {
			if (open.size() <= 0) {
				// If strategy is IDS, and current IDS max level didn't reach global max, continue!
				if (strategy.equals(SearchStrategy.IDS) && iterDepthLimit < Parameters.MAX_DEPTH) {
					iterDepthLimit++;	
					depthLimit = Math.min(iterDepthLimit, Parameters.MAX_DEPTH);
					restartIDS(rootNode);
				} else {
					failed = true;
				}
			} else {
				GPSNode currentNode = open.remove(0);
				closed.add(currentNode);

				if (explosionCounter % 500 == 0) {
					System.out.println(open.size() + "\t\t" + explosionCounter + "\t\t" + currentNode.getCost() + "\t\t" + (System.currentTimeMillis() - totalTime));
					System.out.println(currentNode);
				}

				if (isGoal(currentNode)) {
					finished = true;
					System.out.println(currentNode.getSolution());
					System.out.println("Expanded nodes: " + explosionCounter);
					System.out.println(open.size() + "\t\t" + currentNode.getCost());
					System.out.println("Total time: " + (System.currentTimeMillis() - totalTime));
				} else {
					
					if (currentNode.getCost() < depthLimit) {
						explosionCounter++;
						explode(currentNode);
					}
				}
			}
		}

		if (finished) {
			System.out.println("OK! solution found!");
		} else if (failed) {
			System.err.println("FAILED! solution not found!");
		}
	}

	private  boolean isGoal(GPSNode currentNode) {
		return currentNode.getState() != null
				&& currentNode.getState().isGoal();
	}

	private  boolean explode(GPSNode node) {
		if(problem.getRules() == null){
			System.err.println("No rules!");
			return false;
		}
		
		for (GPSRule rule : problem.getRules()) {
			GPSState newState = null;
			try {
				newState = rule.evalRule(node.getState());
			} catch (NotAppliableException e) {
				// Do nothing
			}
			if (newState != null && !checkOpenAndClosed(node.getCost() + rule.getCost(),newState)) {
				int hValue = 0;
				if (strategy == SearchStrategy.ASTAR || strategy == SearchStrategy.GREEDY){
					hValue = problem.getHValue(newState);
				}
				GPSNode newNode = new GPSNode(newState, node.getCost() + rule.getCost(), hValue);
				newNode.setParent(node);
				
				addNode(newNode);
			}
		}
		return true;
	}
	
	private  boolean checkOpenAndClosed(Integer cost, GPSState state) {
		GPSNode node = usedHash.get(state);
		return (node != null) && node.getCost() < cost;
	}

	/**
	 * clears frontier and explored lists/hashmaps, reinserts rootnode to frontier
	 * so start the iterative depth search again
	 */
	
	private void restartIDS(GPSNode rootNode) {
		open.clear();
		usedHash.clear();
		open.add(rootNode);
		usedHash.put(rootNode.getState(), rootNode);
	}
	
	public abstract void addNode(GPSNode node);
	
}
